/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ir, native, HomeCheckIrHint } from 'qvog-dsl';
import { CheckerUtils, Defects } from '../../../Index';
import { IssueReport } from '../../../model/Defects';
import { ANONYMOUS_METHOD_PREFIX, DEFAULT_ARK_CLASS_NAME, UNKNOWN_FILE_NAME, UNKNOWN_PROJECT_NAME } from 'arkanalyzer';

const SEVERITY = 2;
const DOC_PATH = 'docs/combine-same-arg-animateto-check-dsl.md';
const DESCRIPTION = 'The same animateto is used when the parameters are the same.';
const visited = new Set();
interface WarnInfo {
    stmt: ir.Stmt;
    arkFile: ir.ArkFile;
}
let gFilePath = '';
let gKeyword: string = 'animateTo';
let curFinishedMap = new Map<string, WarnInfo[]>();
let animateToSignatureStr = `@ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.animateTo(@ohosSdk/component/common.d.ts: AnimateParam, @ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.${ANONYMOUS_METHOD_PREFIX}0())`;
let issues: IssueReport[] = [];

export default native<HomeCheckIrHint>({ use: 'ir' })()
    .match((stmt): stmt is ir.Stmt => {
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile()
        if (!arkFile) {
            return false;
        }
        const file = arkFile.getFilePath();
        if (visited.has(file)) {
            return false;
        } else {
            try {
                issues = [];
                visited.add(file);
                check(arkFile);
            } catch (e) {
                console.error(e);
            }
            return true;
        }
    })
    .report((_, { api }): void => {
        for (const result of issues) {
            api.report({
                location: {
                    file: "",
                    range: {
                        start: {
                            line: result.defect.reportLine,
                            column: result.defect.reportColumn
                        },
                        end: {
                            line: result.defect.reportLine,
                            column: result.defect.reportColumn
                        },
                    }
                },
                severity: SEVERITY,
                description: DESCRIPTION,
                docPath: DOC_PATH,
            })
        }
    });

function check(arkFile: ir.ArkFile): void {
    let scene = arkFile.getScene();
    gFilePath = arkFile.getFilePath();
    for (let clazz of arkFile.getClasses()) {
        classProcess(clazz, scene);
    }
    for (let namespace of arkFile.getAllNamespacesUnderThisFile()) {
        for (let clazz of namespace.getClasses()) {
            classProcess(clazz, scene);
        }
    }
};

function classProcess(arkClass: ir.ArkClass, scene: ir.Scene): void {
    if (!arkClass.hasViewTree()) {
        return;
    }
    let viewTreeRoot = arkClass.getViewTree()?.getRoot();
    if (!viewTreeRoot) {
        return;
    }
    for (let child of viewTreeRoot.children) {
        let attributes = child.attributes;
        for (let values of attributes) {
            for (let value of values) {
                curFinishedMap.clear();
                valueProcess(value, scene);
                getIssueReports();
            }
        }
    }
}

function valueProcess(value: string | [ir.Stmt, (ir.Constant | ir.ArkInstanceFieldRef | ir.MethodSignature)[]], scene: ir.Scene): void {
    if (!(value instanceof Array)) {
        return;
    }
    let arrays = value[1];
    for (let array of arrays) {
        if (!(array instanceof ir.MethodSignature)) {
            continue;
        }
        let method = scene.getMethod(array);
        if (method === null) {
            continue;
        }
        let busyMethods = new Set<ir.MethodSignature>();
        findSymbolInMethod(method, scene, busyMethods);
    }
}

function findSymbolInMethod(method: ir.ArkMethod, scene: ir.Scene, busyMethods: Set<ir.MethodSignature>): void {
    const animatetoStmts = method.getBody()?.getCfg().getStmts();
    if (!animatetoStmts) {
        return;
    }
    const curMethodSignature = method.getSignature();
    busyMethods.add(curMethodSignature);
    for (let stmt of animatetoStmts) {
        const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
        if (!invokeExpr) {
            continue;
        }
        const invokeSignature = invokeExpr.getMethodSignature();
        let invokeSignatureStr = invokeSignature.toString();
        if (busyMethods.has(invokeSignature) || invokeSignatureStr.includes(`@${UNKNOWN_PROJECT_NAME}/${UNKNOWN_FILE_NAME}`)) {
            continue;
        }
        let clazz = method.getDeclaringArkClass();
        if (invokeSignatureStr === animateToSignatureStr) {
            animateToCheck(stmt, clazz.getDeclaringArkFile(), scene);
        } else {
            findSymbolInInvokeStmt(stmt, scene, busyMethods);
            let invokeMethod = scene.getMethod(invokeSignature);
            if (invokeMethod === null) {
                continue;
            }
            findSymbolInMethod(invokeMethod, scene, busyMethods);
        }
    }
    busyMethods.delete(curMethodSignature);
}

function animateToCheck(stmt: ir.Stmt, file: ir.ArkFile, scene: ir.Scene): void {
    const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
    if (!invokeExpr) {
        return;
    }
    let arg = invokeExpr.getArg(0);
    if (!(arg instanceof ir.Local)) {
        return;
    }
    let type = arg.getType();
    if (!(type instanceof ir.ClassType)) {
        return;
    }
    let classSignature = type.getClassSignature();
    let code = scene.getClass(classSignature)?.getCode();
    if (!code) {
        return;
    }
    let warnInfoStmt: WarnInfo = {
        stmt: stmt,
        arkFile: file
    };
    if (curFinishedMap.has(code)) {
        let warnInfoList = curFinishedMap.get(code);
        if (warnInfoList) {
            warnInfoList.push(warnInfoStmt);
        }
    } else {
        curFinishedMap.set(code, [warnInfoStmt]);
    }
}

function findSymbolInInvokeStmt(stmt: ir.Stmt, scene: ir.Scene, busyMethods: Set<ir.MethodSignature>): void {
    let invokeArgvs = CheckerUtils.getInvokeExprFromStmt(stmt)?.getArgs();
    if (invokeArgvs) {
        findSymbolInArgs(invokeArgvs, scene, busyMethods);
    }
}

function findSymbolInArgs(invokeArgvs: ir.Value[], scene: ir.Scene, busyMethods: Set<ir.MethodSignature>): void {
    for (let argv of invokeArgvs) {
        let type = argv.getType();
        if (!(type instanceof ir.FunctionType)) {
            continue;
        }
        let methodSignature = type.getMethodSignature();
        let anonymousMethod = scene.getMethod(methodSignature);
        if (anonymousMethod !== null && !busyMethods.has(anonymousMethod.getSignature())) {
            findSymbolInMethod(anonymousMethod, scene, busyMethods);
        }
    }
}

function getIssueReports(): void {
    curFinishedMap.forEach((warnInfoList) => {
        if (warnInfoList.length > 1) {
            warnInfoList.forEach((warnInfo) => {
                reportIssue(warnInfo.arkFile, warnInfo.stmt);
            });
        }
    });
}

function reportIssue(arkFile: ir.ArkFile, stmt: ir.Stmt): void {
    let arkFilePath = arkFile.getFilePath();
    if (arkFilePath !== gFilePath || !stmt) {
        return;
    }
    let originPosition = stmt.getOriginPositionInfo();
    let lineNum = originPosition.getLineNo();
    let orgStmtStr = stmt.getOriginalText();
    if (!orgStmtStr || orgStmtStr.length === 0) {
        return;
    }
    let startCol = -1;
    startCol = originPosition.getColNo() + orgStmtStr.indexOf(gKeyword);
    let endCol = startCol + gKeyword.length - 1;
    if (startCol === -1) {
        return;
    }
    const severity = SEVERITY;
    let defects = new Defects(lineNum, startCol, endCol, DESCRIPTION, severity, "",
        arkFilePath, DOC_PATH, true, false, true);
    issues.push(new IssueReport(defects, undefined));
}